Support integer range keys in constant arrays#4952
Support integer range keys in constant arrays#4952staabm wants to merge 24 commits intophpstan:2.1.xfrom
Conversation
|
You've opened the pull request against the latest branch 2.2.x. PHPStan 2.2 is not going to be released for months. If your code is relevant on 2.1.x and you want it to be released sooner, please rebase your pull request and change its target to 2.1.x. |
- When setting a constant array offset with a finite union of constant string types or a finite IntegerRangeType, produce a union of constant arrays instead of degrading to a general array type - Added resolveFiniteScalarKeyTypes() helper on ConstantArrayType to extract constant key types from string unions and integer ranges - Limited expansion to CHUNK_FINITE_TYPES_LIMIT (5) keys to avoid combinatorial explosion in loop fixpoint analysis - Excluded integer constant unions (e.g., 0|1 from loops) to prevent regression in loop variable tracking - Updated constant-array-type-set.php test expectations for more precise results with int<0,4> range offsets
- Remove unnecessary toArrayKey() calls in resolveFiniteScalarKeyTypes() - Use ConstantArrayTypeBuilder instead of recursion in setOffsetValueType() - Remove CLAUDE.md changes - Add test case for int<0, 5>|int<10, 15> union of integer ranges Co-authored-by: Markus Staab <staabm@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Automated fix attempt 1 for CI failures.
- Remove bug-14129.php (already covered by set-constant-union-offset-on-constant-array.php) - Move bug-7978.php from NSRT to CallMethodsRuleTest - Add de-duplication for overlapping integer ranges in resolveFiniteScalarKeyTypes() - Add test for overlapping ranges (int<0,3>|int<2,4>) Co-authored-by: Markus Staab <staabm@users.noreply.github.com>
|
This pull request has been marked as ready for review. |
| $scalarKeyTypes = $this->resolveFiniteScalarKeyTypes($offsetType); | ||
| // turn into tagged union for more precise results | ||
| if ( | ||
| $scalarKeyTypes !== null | ||
| && count($scalarKeyTypes) >= 2 | ||
| && count($scalarKeyTypes) <= InitializerExprTypeResolver::CALCULATE_SCALARS_LIMIT | ||
| ) { | ||
| $hasNewKey = false; | ||
| foreach ($scalarKeyTypes as $scalarKeyType) { | ||
| $existingKeyFound = false; | ||
| foreach ($this->keyTypes as $existingKeyType) { | ||
| if ($existingKeyType->getValue() === $scalarKeyType->getValue()) { | ||
| $existingKeyFound = true; | ||
| break; | ||
| } | ||
| } | ||
| if (!$existingKeyFound) { | ||
| $hasNewKey = true; | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| if ($hasNewKey) { | ||
| $arrayTypes = []; | ||
| foreach ($scalarKeyTypes as $scalarKeyType) { | ||
| $builder = ConstantArrayTypeBuilder::createFromConstantArray($this); | ||
| $builder->setOffsetValueType($scalarKeyType, $valueType); | ||
| $arrayTypes[] = $builder->getArray(); | ||
| } | ||
|
|
||
| return TypeCombinator::union(...$arrayTypes); | ||
| } | ||
| } |
There was a problem hiding this comment.
in a perfect world, I think this logic should be part of ConstantArrayTypeBuilder->setOffsetValueType
c81e7f1 to
f3e2700
Compare
| if ($offsetType !== null) { | ||
| $scalarKeyTypes = $offsetType->toArrayKey()->getConstantStrings(); | ||
| if (count($scalarKeyTypes) === 0) { | ||
| $integerRanges = TypeUtils::getIntegerRanges($offsetType); |
There was a problem hiding this comment.
You're calling getIntegerRanges on offsetType but previously you worked with
$offsetType->toArrayKey()
Shouldn't you work every where with $offsetType->toArrayKey() ?
that could have impact for things like int<0, 2>|'3' maybe ?
| } | ||
|
|
||
| // turn into tagged union for more precise results | ||
| if ( |
There was a problem hiding this comment.
It's unclear to me why
- We're working only with constantStrings (getConstantStrings)
- We're adding int range only when there is no string
What about @param 1|2 $key ?
And what about @param '1'|2 $key ?
Also the $existingKeyType->getValue() === $scalarKeyType->getValue() check might miss some 1 === '1'.
Summary
When assigning to a constant array with a union of constant string keys (e.g.,
'a'|'b') or a finite integer range (e.g.,int<1,5>), PHPStan previously degraded to a generalArrayType, losing the constant array shape information. This change produces a union of constant arrays instead, preserving precise type information.For example, given
@param array{foo: int} $aand@param int<1,5> $intRange:Changes
src/Type/Constant/ConstantArrayType.php:setOffsetValueType()to detect when the offset type can be expanded to a finite set of constant keys with at least one new key, and produce a union of constant arraysresolveFiniteScalarKeyTypes()private helper method that resolves offset types to individual constant key types from string unions and integer rangesCHUNK_FINITE_TYPES_LIMIT(5) new keys to avoid combinatorial explosionIntegerRangeType(not integer constant unions like0|1which typically come from loop fixpoint analysis)tests/PHPStan/Analyser/nsrt/set-constant-union-offset-on-constant-array.php: New regression test covering string union keys, integer range keys, infinite ranges, and existing keystests/PHPStan/Analyser/nsrt/constant-array-type-set.php: Updated test expectation forint<0, 4>offset on 3-element array to reflect more precise union resultCLAUDE.md: Added documentation about the new union expansion behaviorRoot cause
ConstantArrayTypeBuilder::setOffsetValueType()handles non-constant offset types by expanding them to constant scalars and checking if all match existing keys. When some keys were new (not in the array), it fell through to a degradation path that produced a generalArrayType. The fix intercepts this at theConstantArrayType::setOffsetValueType()level, creating a union of arrays for each possible key before the builder can degrade.The expansion is carefully limited to avoid regressions:
IntegerRangeTypetrigger expansion (not integer constant unions which are common in loop analysis)CHUNK_FINITE_TYPES_LIMIT(5) new keys are expandedTest
Added
tests/PHPStan/Analyser/nsrt/set-constant-union-offset-on-constant-array.phpwith four test cases:doFoo: String union offset'a'|'b'onarray{foo: int}→ union of two constant arraysdoBar: Integer rangeint<1,5>onarray{foo: int}→ union of five constant arraysdoInfiniteRange: Infinite rangeint<0, max>→ falls back to general array (no expansion)doExistingKeys: Rangeint<0,1>onarray{0: 'a', 1: 'b'}→ handled by builder directlyFixes phpstan/phpstan#14129
Closes phpstan/phpstan#9907